Laurel: Add constrained type support#505
Merged
fabiomadge merged 11 commits intomainfrom Mar 12, 2026
Merged
Conversation
1c60132 to
b2d41d6
Compare
579e349 to
dd1139d
Compare
fa3f500 to
566122e
Compare
566122e to
e2ff8b6
Compare
9c8f422 to
afd9ee8
Compare
keyboardDrummer
requested changes
Mar 9, 2026
Contributor
keyboardDrummer
left a comment
There was a problem hiding this comment.
Code quality looks great but I have some feedback on the design.
Also, could you add a test that compares the Laurel before and after the phase? Similar to StrataTest/Languages/Laurel/LiftExpressionAssignmentsTest.lean
StrataTest/Languages/Laurel/Examples/Fundamentals/T09_ConstrainedTypes.lean
Outdated
Show resolved
Hide resolved
56da20a to
d4f2563
Compare
keyboardDrummer
previously approved these changes
Mar 10, 2026
shigoel
reviewed
Mar 10, 2026
shigoel
reviewed
Mar 10, 2026
f210ea6
056055c to
f210ea6
Compare
github-merge-queue bot
pushed a commit
that referenced
this pull request
Mar 12, 2026
Change the `LocalVariable none` case to emit Core `init` without RHS (havoc) instead of `defaultExprForType`. Source language translators that need default values should add explicit initializers at the Laurel level. This is the correct semantics for Laurel as a verification IR: an uninitialized variable has no known value until assigned. The previous behavior (defaulting to 0/false/"") was introduced in #435 for Python→Laurel but doesn't generalize to other source languages (e.g., JS/TS where uninitialized means `undefined`, not 0). Enables a follow-up in #505 to switch constrained type uninitialized variables from witness injection + assert to assume. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. Co-authored-by: Shilpi Goel <shigoel@gmail.com>
A Laurel-to-Laurel elimination pass (ConstrainedTypeElim.lean) that: - Adds requires for constrained-typed inputs - Adds ensures for constrained-typed outputs - Clears isFunctional when adding ensures (function postconditions not yet supported) - Inserts assert for local variable init and reassignment - Uses witness as default initializer for uninitialized constrained variables - Validates witnesses via synthetic procedures - Injects constraints into quantifier bodies (forall → implies, exists → and) - Resolves all constrained type references to base types - Handles capture avoidance in identifier substitution Core's call elimination handles caller-side argument asserts and return value assumes automatically via requires/ensures. Grammar: constrained type syntax Parser: parseConstrainedType + topLevelConstrainedType Test: T09_ConstrainedTypes — 25 test procedures
Save/restore PredVarMap state around Block, IfThenElse, and While via inScope helper to prevent constrained variable entries from leaking across sibling scopes. Reported by shigoel: a constrained variable declared in an if-branch would cause spurious asserts on same-named variables in sibling blocks.
Generate named constraint functions (e.g. nat$constraint) instead of inlining substituted constraint expressions. This eliminates substId (with its partial annotation and capture avoidance complexity) and produces clearer Core output with named function calls. Constraint functions are placed before user procedures to ensure resolution processes them first (workaround for resolution ID assignment order dependency).
Generate named constraint functions (e.g. nat$constraint) instead of inlining substituted constraint expressions. This eliminates substId (with its partial annotation and capture avoidance complexity) and produces clearer Core output with named function calls. Constraint functions are placed before user procedures so that resolution assigns their IDs before resolving references in user procedure bodies. Also changes posnat constraint to x != 0 per review suggestion.
…rained variables Include translator fix: emit init without RHS (havoc) for uninitialized variables instead of defaultExprForType. For uninitialized constrained variables, emit assume instead of assert: var x: posint; → var x: int; assume posint$constraint(x); The witness is now only used in witness validation procedures.
0f8fc58 to
7526c9f
Compare
MikaelMayer
reviewed
Mar 12, 2026
MikaelMayer
previously approved these changes
Mar 12, 2026
ac8acc2 to
f75d8b8
Compare
MikaelMayer
approved these changes
Mar 12, 2026
shigoel
approved these changes
Mar 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Constrained Types for Laurel
Adds constrained types to Laurel via a Laurel-to-Laurel elimination pass that inserts verification checks at type boundaries.
Syntax
How it works
The pass (
ConstrainedTypeElim.lean) generates a named constraint function per constrained type (e.g.function nat$constraint(x: int): bool { x >= 0 }) and eliminates constrained types by:requires constraintFunc(param)for constrained-typed inputs — Core handles caller asserts and body assumes via call eliminationensures constraintFunc(result)for constrained-typed outputs — Core handles body checks and caller assumesassert constraintFunc(var)after local variable init and reassignment of constrained-typed variablesforall(n: nat) => bodybecomesforall(n: int) => nat$constraint(n) ==> body;existsuses&&The Core translator sees only base types and regular requires/ensures/assert with constraint function calls — no translator changes needed beyond pipeline wiring.
Functions
Functions (
isFunctionalprocedures) with constrained return types emit a "constrained return types on functions are not yet supported" diagnostic. The function is translated as-is with types resolved, but the return constraint is not checked.Changes
ConstrainedTypeElim.lean— the elimination passConstrainedTypeElimTest.lean— before/after comparison test and scope regression testLaurelGrammar.st/LaurelGrammar.lean— constrained type syntaxConcreteToAbstractTreeTranslator.lean— parser forconstrainedkeywordLaurelToCoreTranslator.lean— pipeline wiring (import + pass + resolve + diagnostics)T10_ConstrainedTypes.lean— 24 test procedures covering inputs, outputs, assignments, arguments, nested types, functions, witnesses, quantifiers, capture avoidanceKnown limitations
resolveBaseTypeispartial— cyclic constrained type definitions loop foreverBy submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.